﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Effects.Wave
{
    /// <summary>
    ///  Base class for all chunks.
    /// </summary>
    /// <remarks>
    ///  For definition of chunk and format of wave file please check <see cref="http://www.lightlink.com/tjweber/StripWav/WAVE.html"/>
    /// </remarks>
    public class Chunk
    {
        #region chunk_porperties
        // id and lenght properties

        /// <summary>
        ///  Length of the chunk in bytes with offset 8 - not including Id and length itself  
        /// </summary>
        protected uint length;

        /// <summary>
        ///  Gets length of the chunk in bytes with offset 8 - not including Id and length itself
        /// </summary>
        public uint Lenght
        {
            get{return length;}
        }

        /// <summary>
        ///  Id of the chunk - 4 characters long
        /// </summary>
        protected string id;

        /// <summary>
        ///  Gets Id of the chunk - 4 characters long string
        /// </summary>
        public string Id
        {
            get { return id; }
        }

        #endregion

        #region constructor
        // constructor

        /// <summary>
        ///   Creates chunk with given length and id
        /// </summary>
        /// <param name="length"> length of the chunk in bytes - not including 4 bytes for id and 4 for lenght itself </param>
        /// <param name="id"> Id of the chunk </param>
        public Chunk(uint length, string id)
        {
            this.length = length;
            this.id = id;
        }

        #endregion

        #region chunk_to_write_method
        // stuff for preparing data for writing

        /// <summary>
        ///  Gets byte array that is ready to be written into the file
        /// </summary>
        /// <returns>
        ///     Byte array that is ready to be written into the file
        /// </returns>
        public virtual byte[] ChunkToWrite()
        {
            byte[] idByte = Encoding.ASCII.GetBytes(id);
            byte[] lenByte = BitConverter.GetBytes(length);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(lenByte);

            byte[] ret = new byte[idByte.Length + lenByte.Length];
            Array.Copy(idByte, ret, idByte.Length);
            Array.Copy(lenByte, 0, ret, idByte.Length, lenByte.Length);

            return ret;
        }

        #endregion
    }

    /// <summary>
    ///  Class represents FmtChunk (Format chunk in WAVE file)
    /// </summary>
    public class FmtChunk : Chunk
    {
        #region properties 
        // fields and properties of the chunk.

        /// <summary>
        ///  format tag specifies compresion used
        /// </summary>
        protected ushort formatTag;
        /// <summary>
        ///  Gets format tag
        /// </summary>
        public ushort Format
        {
            get { return formatTag; }
        }

        // number of channels stored in file
        /// <summary>
        ///  number of channels
        /// </summary>
        protected ushort channels;
        /// <summary>
        ///  Gets the number of channels.
        /// </summary>
        public ushort Channels
        {
            get { return channels; }
        }

        // sample rate
        /// <summary>
        ///  Sample rate in Hz
        /// </summary>
        protected uint sampleRate;
        /// <summary>
        ///  Gets the sample rate in Hz
        /// </summary>
        public uint SampleRate
        {
            get { return sampleRate; }
        }
        
        // average size needed for one second of recording
        // calculated with the formula sampleRate * frameSize
        /// <summary>
        ///  Average size needed for one second of recording. Calculated with the formula sampleRate * frameSize.
        /// </summary>
        protected uint avgBytesPerSec;
        /// <summary>
        ///  Gets the average size needed for one second of recording.
        ///  There holds: averageBytesPerSec =  sampleRate * frameSize.
        /// </summary>
        public uint AvgBytesPerSec
        {
            get { return avgBytesPerSec; }
        }
        
        // size of one frame, frame means multichannel sample
        /// <summary>
        ///  Size of one frame, frame means multichannel sample
        /// </summary>
        protected ushort frameSize;
        /// <summary>
        ///  Gets the size one frame. Frame means multichannel sample
        /// </summary>
        public ushort FrameSize
        {
            get { return frameSize; }
        }

        /// <summary>
        ///  Bit depth (how many bits are used to stored one sample)
        /// </summary>
        protected ushort bitsPerSample;
        /// <summary>
        ///  Gets the bit depth.
        /// </summary>
        public ushort BitsPerSample
        {
            get { return bitsPerSample;}
        }

        #endregion

        /// <summary>
        ///  Creates FmtChunk that uses given information.
        /// </summary>
        /// <param name="length"> length of a chunk </param>
        /// <param name="formatTag"> format tag </param>
        /// <param name="channels"> channels count </param>
        /// <param name="sampleRate"> sample rate </param>
        /// <param name="avgBytesPerSec"> average bytes per second </param>
        /// <param name="frameSize"> frame size </param>
        /// <param name="bitsPerSample"> bits per sample (bit depth) </param>
        public FmtChunk(uint length,ushort formatTag,ushort channels,uint sampleRate,uint avgBytesPerSec,ushort frameSize,ushort bitsPerSample)
            :base(length,"fmt ")
        {
            
            this.formatTag=formatTag;
            this.channels=channels;
            this.sampleRate=sampleRate;
            this.avgBytesPerSec=avgBytesPerSec;
            this.frameSize=frameSize;
            this.bitsPerSample = bitsPerSample;
        }

        /// <summary>
        ///  Adds byte array buffer to List&lt;byte&gt; list
        /// </summary>
        /// <param name="list"> List&lt;byte&gt; to add to</param>
        /// <param name="buffer"> array that is added </param>
        private static void add(ref List<byte> list, byte[] buffer)
        {
            if (BitConverter.IsLittleEndian)
                foreach (byte b in buffer)
                    list.Add(b);
            else
            {
                Array.Reverse(buffer);
                foreach (byte b in buffer)
                    list.Add(b);
            }
        }

        /// <summary>
        ///  Overridden method
        /// </summary>
        /// <returns> array of bytes that can be written to the file </returns>
        public override byte[] ChunkToWrite()
        {
            byte[] super=base.ChunkToWrite();

            List<byte> ret = new List<byte>();
            foreach (byte b in super)
                ret.Add(b);

            byte[] buffer;

            buffer=BitConverter.GetBytes(formatTag);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            add(ref ret, buffer);

            buffer = BitConverter.GetBytes(channels);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            add(ref ret, buffer);

            buffer = BitConverter.GetBytes(sampleRate);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            add(ref ret, buffer);

            buffer = BitConverter.GetBytes(avgBytesPerSec);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            add(ref ret, buffer);

            buffer = BitConverter.GetBytes(frameSize);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            add(ref ret, buffer);

            buffer = BitConverter.GetBytes(bitsPerSample);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(buffer);
            add(ref ret, buffer);

            return ret.ToArray();
        }
    }

    /// <summary>
    ///  Class represents DataChunk in wave file 
    /// </summary>
    public class DataChunk : Chunk,System.Collections.IEnumerable
    {
        #region data
        // field and property for data stored in chunk

        /// <summary>
        ///  Data stored in data chunk.
        /// </summary>
        /// <remarks>
        ///  Current implementation is completely straithforward. It just holds array of bytes.
        /// </remarks>
        protected double[] data;
        
        /// <summary>
        ///  Gets data stored in DataChunk
        /// </summary>
        public double[] Data
        {
            get { return data; }
        }

        #endregion

        #region indexer
        // indexer

        /// <summary>
        ///   Indexer that accesses Data array
        /// </summary>
        /// <param name="index">index to the Data array</param>
        /// <returns> byte stored at index</returns>
        public double this[long index]
        {
            get
            {
                if (index < 0 || index >= data.Length)
                    throw new IndexOutOfRangeException();

                return data[index];
            }
        }

        #endregion

        #region constructor 

        /// <summary>
        ///  Creates data chunk storing given data
        /// </summary>
        /// <param name="data"> Data in data chunk </param>
        public DataChunk(double[] data)
            :base(Convert.ToUInt32(data.Length),"data")
        {
            this.data = data;
        }

        /// <summary>
        ///  Creates data chunk with specified length.
        /// </summary>
        /// <param name="length"> Lenght of the data chunk in samples </param>
        public DataChunk(int length)
            : base(Convert.ToUInt32(length), "data")
        {
            this.data = new double[length];
        }

        #endregion

        #region iterator
        // enumerator that runs through the stored data

        /// <summary>
        ///  Iterator that runs through the stored bytes
        /// </summary>
        /// <returns>
        ///  Iterator to that runs thorught the Data array. 
        /// </returns>
        public System.Collections.IEnumerator GetEnumerator()
        {
            for (int i = 0; i < this.length; i++)
            {
                yield return data[i];
            }
            yield break;
        }

        #endregion

        /// <summary>
        ///  Deprecated: Method doesn't work properly. Data chunk currenlty stores sample as double values from range -1,1. It has no connection
        ///  with format chunk, so it can't produce raw byte data. 
        /// </summary>
        /// <returns> Deprecated </returns>
        public override byte[] ChunkToWrite()
        {
            byte[] header = base.ChunkToWrite();

            byte[] ret = new byte[data.Length + header.Length];
            Array.Copy(header, 0, ret, 0, header.Length);
            Array.Copy(data, 0, ret, header.Length, data.Length);

            return ret;
        }
    }

}
